import base64Coder from 'Base64'
import Shapes from './Shapes'
import 'mxgraph/javascript/src/css/common.css'

const mxgraph = require('mxgraph')({})
const {
  mxGraph,
  mxUtils,
  mxCodec,
  mxCell,
  mxGeometry,
  mxRectangle,
  mxEvent,
  mxConstants,
  mxClient,
  mxDragSource,
  mxPoint,
  mxImage,
  mxStackLayout,
  mxEventObject,
  mxRubberband,
  mxVertexHandler,
  mxEdgeHandler,
  mxOutline,
  mxGraphHandler,
  mxConnectionHandler,
  mxCellHighlight
} = mxgraph

class Graph {
  _instance = null
  container = null
  graph = null



  constructor(container) {
    if (!(this instanceof Graph)) {
      return
    }
    // 单例模式
    if (!Graph._instance) {
      this.container = container
      this.graph = new mxGraph(container)
      Graph._instance = this
    }

    return Graph._instance
  }
}

const createSvgImage = function (w, h, data, coordWidth, coordHeight) {
  let tmp = unescape(encodeURIComponent(
    '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' +
    '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="' + w + 'px" height="' + h + 'px" ' +
    ((coordWidth != null && coordHeight != null) ? 'viewBox="0 0 ' + coordWidth + ' ' + coordHeight + '" ' : '') +
    'version="1.1">' + data + '</svg>'));

  return new mxImage('data:image/svg+xml;base64,' + ((window.btoa) ? btoa(tmp) : base64Coder.btoa(tmp, true)), w, h)
}

Graph.prototype.arrowFill = '#29b6f2'

Graph.prototype.triangleUp = createSvgImage(18, 28, '<path d="m 6 26 L 12 26 L 12 12 L 18 12 L 9 1 L 1 12 L 6 12 z" stroke="#fff" fill="' + Graph.prototype.arrowFill + '"/>')
Graph.prototype.triangleRight = createSvgImage(26, 18, '<path d="m 1 6 L 14 6 L 14 1 L 26 9 L 14 18 L 14 12 L 1 12 z" stroke="#fff" fill="' + Graph.prototype.arrowFill + '"/>')
Graph.prototype.triangleDown = createSvgImage(18, 26, '<path d="m 6 1 L 6 14 L 1 14 L 9 26 L 18 14 L 12 14 L 12 1 z" stroke="#fff" fill="' + Graph.prototype.arrowFill + '"/>')
Graph.prototype.triangleLeft = createSvgImage(28, 18, '<path d="m 1 9 L 12 1 L 12 6 L 26 6 L 26 12 L 12 12 L 12 18 z" stroke="#fff" fill="' + Graph.prototype.arrowFill + '"/>')
Graph.prototype.roundDrop = createSvgImage(26, 26, '<circle cx="13" cy="13" r="12" stroke="#fff" fill="' + Graph.prototype.arrowFill + '"/>')
Graph.prototype.refreshTarget = new mxImage('', 38, 38);
Graph.prototype.tolerance = (mxClient.IS_TOUCH) ? 6 : 0
Graph.prototype.dropTargetDelay = 200

Graph.prototype.mainHandle = createSvgImage(18, 18, '<circle cx="9" cy="9" r="5" stroke="#fff" fill="' + Graph.prototype.arrowFill + '" stroke-width="1"/>')
Graph.prototype.secondaryHandle = createSvgImage(16, 16, '<path d="m 8 3 L 13 8 L 8 13 L 3 8 z" stroke="#fff" fill="#fca000"/>')
Graph.prototype.terminalHandle = createSvgImage(18, 18, '<circle cx="9" cy="9" r="5" stroke="#fff" fill="' + Graph.prototype.arrowFill + '" stroke-width="1"/><circle cx="9" cy="9" r="2" stroke="#fff" fill="transparent"/>')
Graph.prototype.fixedHandle = createSvgImage(18, 18, '<circle cx="9" cy="9" r="5" stroke="#fff" fill="' + Graph.prototype.arrowFill + '" stroke-width="1"/><path d="m 7 7 L 11 11 M 7 11 L 11 7" stroke="#fff"/>')
Graph.prototype.rotationHandle = createSvgImage(16, 16, '<path stroke="' + Graph.prototype.arrowFill +
'" fill="' + Graph.prototype.arrowFill +
'" d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"/>',
24, 24)

Graph.prototype.initialDefaultStyles = function () {
  console.log('initial default styles')
  let req = mxUtils.load(require('../styles/default.xml'));
  let root = req.getDocumentElement();
  let dec = new mxCodec(root.ownerDocument);
  dec.decode(root, this.graph.getStylesheet())
  this.graph.getStylesheet().styles.defaultEdge.strokeColor = '#000000'

  new mxRubberband(this.graph)
  new Shapes(mxgraph)


  // 修改默认图标样式
  mxVertexHandler.prototype.handleImage = this.mainHandle
  mxVertexHandler.prototype.secondaryHandleImage = this.secondaryHandle;
  mxEdgeHandler.prototype.handleImage = this.mainHandle
  mxEdgeHandler.prototype.terminalHandleImage = this.terminalHandle;
  mxEdgeHandler.prototype.fixedHandleImage = this.fixedHandle;
  mxEdgeHandler.prototype.labelHandleImage = this.secondaryHandle;
  mxOutline.prototype.sizerImage = this.mainHandle

  // 打开形状的开关
  // Adds rotation handle and live preview
  mxVertexHandler.prototype.rotationEnabled = true;
  mxVertexHandler.prototype.manageSizers = true;
  mxVertexHandler.prototype.livePreview = true;
  mxGraphHandler.prototype.maxLivePreview = 16;

  // Increases default rubberband opacity (default is 20)
  mxRubberband.prototype.defaultOpacity = 30;
  
  // Enables connections along the outline, virtual waypoints, parent highlight etc
  mxConnectionHandler.prototype.outlineConnect = true;
  mxCellHighlight.prototype.keepOnTop = true;
  mxVertexHandler.prototype.parentHighlightEnabled = true;
  
  mxEdgeHandler.prototype.parentHighlightEnabled = true;
  mxEdgeHandler.prototype.dblClickRemoveEnabled = true;
  mxEdgeHandler.prototype.straightRemoveEnabled = true;
  mxEdgeHandler.prototype.virtualBendsEnabled = true;
  mxEdgeHandler.prototype.mergeRemoveEnabled = true;
  mxEdgeHandler.prototype.manageLabelHandle = true;
  mxEdgeHandler.prototype.outlineConnect = true;

  // Enables snapping to off-grid terminals for edge waypoints
  mxEdgeHandler.prototype.snapToTerminals = true;
  // 修改旋转图标
  const vertexHandlerCreateSizerShape = mxVertexHandler.prototype.createSizerShape;
  mxVertexHandler.prototype.createSizerShape = function(bounds, index)
  {
    this.handleImage = (index == mxEvent.ROTATION_HANDLE) ? Graph.prototype.rotationHandle : (index == mxEvent.LABEL_HANDLE) ? this.secondaryHandleImage : this.handleImage;
    
    return vertexHandlerCreateSizerShape.apply(this, arguments);
  }
}

Graph.prototype.getGraph = function () {
  return this.graph
}

Graph.prototype.createThumb = function (cells, width, height, parent) {
  // this.graph.labelsVisible = (showLabel == null || showLabel);
  let fo = mxClient.NO_FO
  this.graph.view.scaleAndTranslate(1, 0, 0);
  this.graph.addCells(cells);
  let bounds = this.graph.getGraphBounds();
  let s = Math.floor(Math.min((width - 2 * 2) / bounds.width,
    (height - 2 * 2) / bounds.height) * 100) / 100;
  this.graph.view.scaleAndTranslate(s, Math.floor((width - bounds.width * s) / 2 / s - bounds.x),
    Math.floor((height - bounds.height * s) / 2 / s - bounds.y));
  let node = null;

  // For supporting HTML labels in IE9 standards mode the container is cloned instead
  if (this.graph.dialect == mxConstants.DIALECT_SVG && !mxClient.NO_FO &&
    this.graph.view.getCanvas().ownerSVGElement != null) {
    node = this.graph.view.getCanvas().ownerSVGElement.cloneNode(true);
  }
  // LATER: Check if deep clone can be used for quirks if container in DOM
  else {
    node = this.graph.container.cloneNode(false);
    node.innerHTML = this.graph.container.innerHTML;

    // Workaround for clipping in older IE versions
    if (mxClient.IS_QUIRKS || document.documentMode == 8) {
      node.firstChild.style.overflow = 'visible';
    }
  }

  this.graph.getModel().clear();
  mxClient.NO_FO = fo;

  node.style.position = 'relative';
  node.style.overflow = 'hidden';
  node.style.left = 2 + 'px';
  node.style.top = 2 + 'px';
  node.style.width = width + 'px';
  node.style.height = height + 'px';
  node.style.visibility = '';
  node.style.minWidth = '';
  node.style.minHeight = '';

  parent.appendChild(node);

  return bounds;
}

Graph.prototype.createCurve = function () {
  const cell = new mxCell('', new mxGeometry(0, 0, 50, 50), 'curved=1;endArrow=classic;html=1;');
  cell.geometry.setTerminalPoint(new mxPoint(0, 50), true);
  cell.geometry.setTerminalPoint(new mxPoint(50, 0), false);
  cell.geometry.points = [new mxPoint(50, 50), new mxPoint(0, 0)];
  cell.geometry.relative = true;
  cell.edge = true
  return this.createEdgeTemplateFromCells([cell], cell.geometry.width, cell.geometry.height);
}

Graph.prototype.createEdgeTemplateFromCells = function (cells, width, height) {
  return this.createItem(cells, width, height)
}

Graph.prototype.addGeneralPalette = function (container) {
  const fns = [
    this.createVertexTemplate('rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#000000;', 120, 60, ''),
    this.createVertexTemplate('rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#000000;', 120, 60, ''),
    this.createVertexTemplate('text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;', 40, 20, 'Text'),
    this.createVertexTemplate('text;html=1;strokeColor=none;fillColor=none;spacing=5;spacingTop=-20;whiteSpace=wrap;overflow=hidden;rounded=0;', 190, 120,
      '<h1>Heading</h1><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>'),
    this.createVertexTemplate('shape=ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#000000;', 120, 80, ''),
    this.createVertexTemplate('shape=process;whiteSpace=wrap;html=1;backgroundOutline=1;fillColor=#ffffff;strokeColor=#000000;', 120, 60, ''),
    this.createCurve()
  ]
  this.addPaletteFunctions(container, fns)
}

Graph.prototype.addPaletteFunctions = function (container, fns) {
  for (let i = 0; i < fns.length; i++) {
    container.appendChild(fns[i]);
  }
}

Graph.prototype.createVertexTemplate = function (style, width, height, value) {
  let cells = [new mxCell((value != null) ? value : '', new mxGeometry(0, 0, width, height), style)];
  cells[0].vertex = true
  return this.createItem(cells, width, height)
}

Graph.prototype.createItem = function (cells, width, height) {
  const elt = document.createElement('a')
  elt.className = 'geItem';
  elt.style.overflow = 'hidden';
  let border = 4;
  elt.style.width = (42 + border) + 'px';
  elt.style.height = (42 + border) + 'px';
  elt.style.padding = 2 + 'px'
  const bounds = new mxRectangle(0, 0, width, height)
  this.createThumb(cells, 42, 42, elt, width, height)
  if (cells.length > 1 || cells[0].vertex) {
    const ds = this.createDragSource(elt, this.createDropHandler(cells, true, bounds), this.createDragPreview(width, height), cells, bounds)
    this.addClickHandler(elt, ds, cells);

    // Uses guides for vertices only if enabled in graph
    ds.isGuidesEnabled = mxUtils.bind(this, function () {
      return this.graph.graphHandler.guidesEnabled;
    });
  }
  else if (cells[0] != null && cells[0].edge) {
    const ds = this.createDragSource(elt, this.createDropHandler(cells, false, bounds), this.createDragPreview(width, height), cells, bounds)
    this.addClickHandler(elt, ds, cells);
  }


  return elt
}

Graph.prototype.createDropHandler = function (cells, allowSplit, bounds) {
  return mxUtils.bind(this, function (graph, evt, target, x, y, force) {
    let elt = (force) ? null : ((mxEvent.isTouchEvent(evt) || mxEvent.isPenEvent(evt)) ? document.elementFromPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt)) : mxEvent.getSource(evt))

    while (elt != null && elt != self.container) {
      elt = elt.parentNode;
    }

    if (elt == null && graph.isEnabled()) {
      cells = graph.getImportableCells(cells);

      if (cells.length > 0) {
        graph.stopEditing();

        // Holding alt while mouse is released ignores drop target
        let validDropTarget = (target != null && !mxEvent.isAltDown(evt)) ? graph.isValidDropTarget(target, cells, evt) : false
        let select = null

        if (target != null && !validDropTarget) {
          target = null
        }

        if (!graph.isCellLocked(target || graph.getDefaultParent())) {
          graph.model.beginUpdate()
          try {
            x = Math.round(x)
            y = Math.round(y)

            // Splits the target edge or inserts into target group
            if (allowSplit && graph.isSplitTarget(target, cells, evt)) {
              let s = graph.view.scale
              let tr = graph.view.translate
              let tx = (x + tr.x) * s
              let ty = (y + tr.y) * s

              let clones = graph.cloneCells(cells)
              graph.splitEdge(target, clones, null,
                x - bounds.width / 2, y - bounds.height / 2,
                tx, ty)
              select = clones
            }
            else if (cells.length > 0) {
              select = graph.importCells(cells, x, y, target);
            }

            // Executes parent layout hooks for position/order
            if (graph.layoutManager != null) {
              let layout = graph.layoutManager.getLayout(target);

              if (layout != null) {
                let s = graph.view.scale
                let tr = graph.view.translate
                let tx = (x + tr.x) * s
                let ty = (y + tr.y) * s

                for (let i = 0; i < select.length; i++) {
                  layout.moveCell(select[i], tx, ty)
                }
              }
            }
          }
          catch (e) {
            console.log(e)
          }
          finally {
            graph.model.endUpdate()
          }

          if (select != null && select.length > 0) {
            graph.scrollCellToVisible(select[0])
            graph.setSelectionCells(select)
          }

          if (graph.editAfterInsert && evt != null && mxEvent.isMouseEvent(evt) &&
            select != null && select.length == 1) {
            window.setTimeout(function () {
              graph.startEditing(select[0])
            }, 0)
          }
        }
      }
      mxEvent.consume(evt);
    }
  })
}

Graph.prototype.createDragPreview = function (width, height) {
  const elt = document.createElement('div')

  elt.style.border = '2px dotted black'
  elt.style.width = `${width}px`
  elt.style.height = `${height}px`
  return elt;
}

Graph.prototype.createDragSource = function (elt, dropHandler, preview, cells, bounds) {
  // Checks if the cells contain any vertices
  let graphInstance = this.graph;
  let freeSourceEdge = null;
  let firstVertex = null;
  let self = this;

  for (let i = 0; i < cells.length; i++) {
    if (firstVertex == null && graphInstance.model.isVertex(cells[i])) {
      firstVertex = i;
    }
    else if (freeSourceEdge == null && graphInstance.model.isEdge(cells[i]) &&
      graphInstance.model.getTerminal(cells[i], true) == null) {
      freeSourceEdge = i;
    }

    if (firstVertex != null && freeSourceEdge != null) {
      break;
    }
  }

  let dropStyleEnabled = this.isDropStyleEnabled(cells, firstVertex);

  let dragSource = mxUtils.makeDraggable(elt, graphInstance, mxUtils.bind(this, function (graph, evt) {
    if (this.updateThread != null) {
      window.clearTimeout(this.updateThread);
    }

    if (cells != null && currentStyleTarget != null && activeArrow == styleTarget) {
      let tmp = graph.isCellSelected(currentStyleTarget.cell) ? graph.getSelectionCells() : [currentStyleTarget.cell];
      let updatedCells = self.updateShapes((graph.model.isEdge(currentStyleTarget.cell)) ? cells[0] : cells[firstVertex], tmp);
      graph.setSelectionCells(updatedCells);
    }
    else if (cells != null && activeArrow != null && currentTargetState != null && activeArrow != styleTarget) {
      let index = (graph.model.isEdge(currentTargetState.cell) || freeSourceEdge == null) ? firstVertex : freeSourceEdge;
      graph.setSelectionCells(self.dropAndConnect(currentTargetState.cell, cells, direction, index, evt));
    }
    else {
      dropHandler.apply(self, arguments);
    }

    // if (this.editorUi.hoverIcons != null)
    // {
    // 	this.editorUi.hoverIcons.update(graph.view.getState(graph.getSelectionCell()));
    // }
  }), preview, 0, 0, graphInstance.autoscroll, true, true);

  // Stops dragging if cancel is pressed
  graphInstance.addListener(mxEvent.ESCAPE, function () {
    if (dragSource.isActive()) {
      dragSource.reset();
    }
  });

  // Overrides mouseDown to ignore popup triggers
  let mouseDown = dragSource.mouseDown;

  dragSource.mouseDown = function (evt) {
    if (!mxEvent.isPopupTrigger(evt) && !mxEvent.isMultiTouchEvent(evt)) {
      graphInstance.stopEditing();
      mouseDown.apply(this, arguments);
    }
  };

  // Workaround for event redirection via image tag in quirks and IE8
  function createArrow(img, tooltip) {
    let arrow = null;

    if (mxClient.IS_IE && !mxClient.IS_SVG) {
      // Workaround for PNG images in IE6
      if (mxClient.IS_IE6 && document.compatMode != 'CSS1Compat') {
        arrow = document.createElement(mxClient.VML_PREFIX + ':image');
        arrow.setAttribute('src', img.src);
        arrow.style.borderStyle = 'none';
      }
      else {
        arrow = document.createElement('div');
        arrow.style.backgroundImage = 'url(' + img.src + ')';
        arrow.style.backgroundPosition = 'center';
        arrow.style.backgroundRepeat = 'no-repeat';
      }

      arrow.style.width = (img.width + 4) + 'px';
      arrow.style.height = (img.height + 4) + 'px';
      arrow.style.display = (mxClient.IS_QUIRKS) ? 'inline' : 'inline-block';
    }
    else {
      arrow = mxUtils.createImage(img.src);
      arrow.style.width = img.width + 'px';
      arrow.style.height = img.height + 'px';
    }

    if (tooltip != null) {
      arrow.setAttribute('title', tooltip);
    }

    mxUtils.setOpacity(arrow, (img == self.refreshTarget) ? 30 : 20);
    arrow.style.position = 'absolute';
    arrow.style.cursor = 'crosshair';

    return arrow;
  }

  let currentTargetState = null;
  let currentStateHandle = null;
  let currentStyleTarget = null;
  let activeTarget = false;

  let arrowUp = createArrow(self.triangleUp, 'Connect');
  let arrowRight = createArrow(self.triangleRight, 'Connect');
  let arrowDown = createArrow(self.triangleDown, 'Connect');
  let arrowLeft = createArrow(self.triangleLeft, 'Connect');
  let styleTarget = createArrow(self.refreshTarget, 'Replace');
  // Workaround for actual parentNode not being updated in old IE
  let styleTargetParent = null;
  let roundSource = createArrow(self.roundDrop);
  let roundTarget = createArrow(self.roundDrop);
  let direction = mxConstants.DIRECTION_NORTH;
  let activeArrow = null;

  function checkArrow(x, y, bounds, arrow) {
    if (arrow.parentNode != null) {
      if (mxUtils.contains(bounds, x, y)) {
        mxUtils.setOpacity(arrow, 100);
        activeArrow = arrow;
      }
      else {
        mxUtils.setOpacity(arrow, (arrow == styleTarget) ? 30 : 20);
      }
    }

    return bounds;
  }

  // Hides guides and preview if target is active
  let dsCreatePreviewElement = dragSource.createPreviewElement;

  // Stores initial size of preview element
  dragSource.createPreviewElement = function () {
    let elt = dsCreatePreviewElement.apply(this, arguments);

    // Pass-through events required to tooltip on replace shape
    if (mxClient.IS_SVG) {
      elt.style.pointerEvents = 'none';
    }

    this.previewElementWidth = elt.style.width;
    this.previewElementHeight = elt.style.height;

    return elt;
  };

  // Shows/hides hover icons
  let dragEnter = dragSource.dragEnter;
  dragSource.dragEnter = function () {
    // if (ui.hoverIcons != null)
    // {
    // 	ui.hoverIcons.setDisplay('none');
    // }

    dragEnter.apply(this, arguments);
  };

  let dragExit = dragSource.dragExit;
  dragSource.dragExit = function () {
    // if (ui.hoverIcons != null)
    // {
    // 	ui.hoverIcons.setDisplay('');
    // }

    dragExit.apply(this, arguments);
  };

  dragSource.dragOver = function (graph) {
    mxDragSource.prototype.dragOver.apply(this, arguments);

    if (this.currentGuide != null && activeArrow != null) {
      this.currentGuide.hide();
    }

    if (this.previewElement != null) {
      let view = graph.view;

      if (currentStyleTarget != null && activeArrow == styleTarget) {
        this.previewElement.style.display = (graph.model.isEdge(currentStyleTarget.cell)) ? 'none' : '';

        this.previewElement.style.left = currentStyleTarget.x + 'px';
        this.previewElement.style.top = currentStyleTarget.y + 'px';
        this.previewElement.style.width = currentStyleTarget.width + 'px';
        this.previewElement.style.height = currentStyleTarget.height + 'px';
      }
      else if (currentTargetState != null && activeArrow != null) {
        if (dragSource.currentHighlight != null && dragSource.currentHighlight.state != null) {
          dragSource.currentHighlight.hide();
        }

        let index = (graph.model.isEdge(currentTargetState.cell) || freeSourceEdge == null) ? firstVertex : freeSourceEdge;
        let geo = self.getDropAndConnectGeometry(currentTargetState.cell, cells[index], direction, cells);
        let geo2 = (!graph.model.isEdge(currentTargetState.cell)) ? graph.getCellGeometry(currentTargetState.cell) : null;
        let geo3 = graph.getCellGeometry(cells[index]);
        let parent = graph.model.getParent(currentTargetState.cell);
        let dx = view.translate.x * view.scale;
        let dy = view.translate.y * view.scale;

        if (geo2 != null && !geo2.relative && graph.model.isVertex(parent) && parent != view.currentRoot) {
          let pState = view.getState(parent);

          dx = pState.x;
          dy = pState.y;
        }

        let dx2 = geo3.x;
        let dy2 = geo3.y;

        // Ignores geometry of edges
        if (graph.model.isEdge(cells[index])) {
          dx2 = 0;
          dy2 = 0;
        }

        // Shows preview at drop location
        this.previewElement.style.left = ((geo.x - dx2) * view.scale + dx) + 'px';
        this.previewElement.style.top = ((geo.y - dy2) * view.scale + dy) + 'px';

        if (cells.length == 1) {
          this.previewElement.style.width = (geo.width * view.scale) + 'px';
          this.previewElement.style.height = (geo.height * view.scale) + 'px';
        }

        this.previewElement.style.display = '';
      }
      else if (dragSource.currentHighlight.state != null &&
        graph.model.isEdge(dragSource.currentHighlight.state.cell)) {
        // Centers drop cells when splitting edges
        this.previewElement.style.left = Math.round(parseInt(this.previewElement.style.left) -
          bounds.width * view.scale / 2) + 'px';
        this.previewElement.style.top = Math.round(parseInt(this.previewElement.style.top) -
          bounds.height * view.scale / 2) + 'px';
      }
      else {
        this.previewElement.style.width = this.previewElementWidth;
        this.previewElement.style.height = this.previewElementHeight;
        this.previewElement.style.display = '';
      }
    }
  };

  let startTime = new Date().getTime();
  let timeOnTarget = 0;
  let prev = null;

  // Gets source cell style to compare shape below
  let sourceCellStyle = self.graph.getCellStyle(cells[0]);
  // Allows drop into cell only if target is a valid root
  dragSource.getDropTarget = mxUtils.bind(this, function (graph, x, y, evt) {
    // Alt means no targets at all
    // LATER: Show preview where result will go
    let cell = (!mxEvent.isAltDown(evt) && cells != null) ?
      graph.getCellAt(x, y, null, null, null, function (state) {
        return self.isContainer(state.cell);
      }) : null;
    // Uses connectable parent vertex if one exists
    if (cell != null && !this.graph.isCellConnectable(cell) &&
      !this.graph.model.isEdge(cell)) {
      let parent = this.graph.getModel().getParent(cell);

      if (this.graph.getModel().isVertex(parent) &&
        this.graph.isCellConnectable(parent)) {
        cell = parent;
      }
    }

    // Ignores locked cells
    if (graph.isCellLocked(cell)) {
      cell = null;
    }

    let state = graph.view.getState(cell);
    activeArrow = null;
    let bbox = null;

    // Time on target
    if (prev != state) {
      startTime = new Date().getTime();
      timeOnTarget = 0;
      prev = state;

      if (this.updateThread != null) {
        window.clearTimeout(this.updateThread);
      }
      if (state != null) {
        this.updateThread = window.setTimeout(function () {
          if (activeArrow == null) {
            prev = state;
            dragSource.getDropTarget(graph, x, y, evt);
          }
        }, this.dropTargetDelay + 10);
      }
    }
    else {
      timeOnTarget = new Date().getTime() - startTime;
    }

    // Shift means disabled, delayed on cells with children, shows after this.dropTargetDelay, hides after 2500ms
    if (dropStyleEnabled && (timeOnTarget < 2500) && state != null && !mxEvent.isShiftDown(evt) &&
      // If shape is equal or target has no stroke, fill and gradient then use longer delay except for images
      (((mxUtils.getValue(state.style, mxConstants.STYLE_SHAPE) != mxUtils.getValue(sourceCellStyle, mxConstants.STYLE_SHAPE) &&
        (mxUtils.getValue(state.style, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE) != mxConstants.NONE ||
          mxUtils.getValue(state.style, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE) != mxConstants.NONE ||
          mxUtils.getValue(state.style, mxConstants.STYLE_GRADIENTCOLOR, mxConstants.NONE) != mxConstants.NONE)) ||
        mxUtils.getValue(sourceCellStyle, mxConstants.STYLE_SHAPE) == 'image') ||
        timeOnTarget > 1500 || graph.model.isEdge(state.cell)) && (timeOnTarget > this.dropTargetDelay) &&
      !this.isDropStyleTargetIgnored(state) && ((graph.model.isVertex(state.cell) && firstVertex != null) ||
        (graph.model.isEdge(state.cell) && graph.model.isEdge(cells[0])))) {
      currentStyleTarget = state;
      let tmp = (graph.model.isEdge(state.cell)) ? graph.view.getPoint(state) :
        new mxPoint(state.getCenterX(), state.getCenterY());
      tmp = new mxRectangle(tmp.x - self.refreshTarget.width / 2, tmp.y - self.refreshTarget.height / 2,
        self.refreshTarget.width, self.refreshTarget.height);

      styleTarget.style.left = Math.floor(tmp.x) + 'px';
      styleTarget.style.top = Math.floor(tmp.y) + 'px';

      if (styleTargetParent == null) {
        graph.container.appendChild(styleTarget);
        styleTargetParent = styleTarget.parentNode;
      }

      checkArrow(x, y, tmp, styleTarget);
    }
    // Does not reset on ignored edges
    else if (currentStyleTarget == null || !mxUtils.contains(currentStyleTarget, x, y) ||
      (timeOnTarget > 1500 && !mxEvent.isShiftDown(evt))) {
      currentStyleTarget = null;

      if (styleTargetParent != null) {
        styleTarget.parentNode.removeChild(styleTarget);
        styleTargetParent = null;
      }
    }
    else if (currentStyleTarget != null && styleTargetParent != null) {
      // Sets active Arrow as side effect
      let tmp = (graph.model.isEdge(currentStyleTarget.cell)) ? graph.view.getPoint(currentStyleTarget) : new mxPoint(currentStyleTarget.getCenterX(), currentStyleTarget.getCenterY());
      tmp = new mxRectangle(tmp.x - self.refreshTarget.width / 2, tmp.y - self.refreshTarget.height / 2,
        self.refreshTarget.width, self.refreshTarget.height);
      checkArrow(x, y, tmp, styleTarget);
    }

    // Checks if inside bounds
    if (activeTarget && currentTargetState != null && !mxEvent.isAltDown(evt) && activeArrow == null) {
      // LATER: Use hit-detection for edges
      bbox = mxRectangle.fromRectangle(currentTargetState);

      if (graph.model.isEdge(currentTargetState.cell)) {
        let pts = currentTargetState.absolutePoints;

        if (roundSource.parentNode != null) {
          let p0 = pts[0];
          bbox.add(checkArrow(x, y, new mxRectangle(p0.x - self.roundDrop.width / 2,
            p0.y - self.roundDrop.height / 2, self.roundDrop.width, self.roundDrop.height), roundSource));
        }

        if (roundTarget.parentNode != null) {
          let pe = pts[pts.length - 1];
          bbox.add(checkArrow(x, y, new mxRectangle(pe.x - self.roundDrop.width / 2,
            pe.y - self.roundDrop.height / 2,
            self.roundDrop.width, self.roundDrop.height), roundTarget));
        }
      }
      else {
        let bds = mxRectangle.fromRectangle(currentTargetState);

        // Uses outer bounding box to take rotation into account
        if (currentTargetState.shape != null && currentTargetState.shape.boundingBox != null) {
          bds = mxRectangle.fromRectangle(currentTargetState.shape.boundingBox);
        }

        bds.grow(self.graph.tolerance);
        bds.grow(2);

        let handler = self.graph.selectionCellsHandler.getHandler(currentTargetState.cell);

        if (handler != null) {
          bds.x -= handler.horizontalOffset / 2;
          bds.y -= handler.verticalOffset / 2;
          bds.width += handler.horizontalOffset;
          bds.height += handler.verticalOffset;

          // Adds bounding box of rotation handle to avoid overlap
          if (handler.rotationShape != null && handler.rotationShape.node != null &&
            handler.rotationShape.node.style.visibility != 'hidden' &&
            handler.rotationShape.node.style.display != 'none' &&
            handler.rotationShape.boundingBox != null) {
            bds.add(handler.rotationShape.boundingBox);
          }
        }

        bbox.add(checkArrow(x, y, new mxRectangle(currentTargetState.getCenterX() - this.triangleUp.width / 2,
          bds.y - this.triangleUp.height, this.triangleUp.width, this.triangleUp.height), arrowUp));
        bbox.add(checkArrow(x, y, new mxRectangle(bds.x + bds.width,
          currentTargetState.getCenterY() - this.triangleRight.height / 2,
          this.triangleRight.width, this.triangleRight.height), arrowRight));
        bbox.add(checkArrow(x, y, new mxRectangle(currentTargetState.getCenterX() - this.triangleDown.width / 2,
          bds.y + bds.height, this.triangleDown.width, this.triangleDown.height), arrowDown));
        bbox.add(checkArrow(x, y, new mxRectangle(bds.x - this.triangleLeft.width,
          currentTargetState.getCenterY() - this.triangleLeft.height / 2,
          this.triangleLeft.width, this.triangleLeft.height), arrowLeft));
      }

      // Adds tolerance
      if (bbox != null) {
        bbox.grow(10);
      }
    }

    direction = mxConstants.DIRECTION_NORTH;

    if (activeArrow == arrowRight) {
      direction = mxConstants.DIRECTION_EAST;
    }
    else if (activeArrow == arrowDown || activeArrow == roundTarget) {
      direction = mxConstants.DIRECTION_SOUTH;
    }
    else if (activeArrow == arrowLeft) {
      direction = mxConstants.DIRECTION_WEST;
    }

    if (currentStyleTarget != null && activeArrow == styleTarget) {
      state = currentStyleTarget;
    }

    let validTarget = (firstVertex == null || graph.isCellConnectable(cells[firstVertex])) &&
      ((graph.model.isEdge(cell) && firstVertex != null) ||
        (graph.model.isVertex(cell) && graph.isCellConnectable(cell)));

    // Drop arrows shown after this.dropTargetDelay, hidden after 5 secs, switches arrows after 500ms
    if ((currentTargetState != null && timeOnTarget >= 5000) ||
      (currentTargetState != state &&
        (bbox == null || !mxUtils.contains(bbox, x, y) ||
          (timeOnTarget > 500 && activeArrow == null && validTarget)))) {
      activeTarget = false;
      currentTargetState = ((timeOnTarget < 5000 && timeOnTarget > this.dropTargetDelay) ||
        graph.model.isEdge(cell)) ? state : null;

      if (currentTargetState != null && validTarget) {
        let elts = [roundSource, roundTarget, arrowUp, arrowRight, arrowDown, arrowLeft];

        for (let i = 0; i < elts.length; i++) {
          if (elts[i].parentNode != null) {
            elts[i].parentNode.removeChild(elts[i]);
          }
        }

        if (graph.model.isEdge(cell)) {
          let pts = state.absolutePoints;

          if (pts != null) {
            let p0 = pts[0];
            let pe = pts[pts.length - 1];
            // let tol = graph.tolerance;
            // let box = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol);

            roundSource.style.left = Math.floor(p0.x - self.roundDrop.width / 2) + 'px';
            roundSource.style.top = Math.floor(p0.y - self.roundDrop.height / 2) + 'px';

            roundTarget.style.left = Math.floor(pe.x - self.roundDrop.width / 2) + 'px';
            roundTarget.style.top = Math.floor(pe.y - self.roundDrop.height / 2) + 'px';

            if (graph.model.getTerminal(cell, true) == null) {
              graph.container.appendChild(roundSource);
            }

            if (graph.model.getTerminal(cell, false) == null) {
              graph.container.appendChild(roundTarget);
            }
          }
        }
        else {
          let bds = mxRectangle.fromRectangle(state);

          // Uses outer bounding box to take rotation into account
          if (state.shape != null && state.shape.boundingBox != null) {
            bds = mxRectangle.fromRectangle(state.shape.boundingBox);
          }

          bds.grow(this.graph.tolerance);
          bds.grow(2);

          let handler = this.graph.selectionCellsHandler.getHandler(state.cell);

          if (handler != null) {
            bds.x -= handler.horizontalOffset / 2;
            bds.y -= handler.verticalOffset / 2;
            bds.width += handler.horizontalOffset;
            bds.height += handler.verticalOffset;

            // Adds bounding box of rotation handle to avoid overlap
            if (handler.rotationShape != null && handler.rotationShape.node != null &&
              handler.rotationShape.node.style.visibility != 'hidden' &&
              handler.rotationShape.node.style.display != 'none' &&
              handler.rotationShape.boundingBox != null) {
              bds.add(handler.rotationShape.boundingBox);
            }
          }

          arrowUp.style.left = Math.floor(state.getCenterX() - this.triangleUp.width / 2) + 'px';
          arrowUp.style.top = Math.floor(bds.y - this.triangleUp.height) + 'px';

          arrowRight.style.left = Math.floor(bds.x + bds.width) + 'px';
          arrowRight.style.top = Math.floor(state.getCenterY() - this.triangleRight.height / 2) + 'px';

          arrowDown.style.left = arrowUp.style.left
          arrowDown.style.top = Math.floor(bds.y + bds.height) + 'px';

          arrowLeft.style.left = Math.floor(bds.x - this.triangleLeft.width) + 'px';
          arrowLeft.style.top = arrowRight.style.top;

          if (state.style['portConstraint'] != 'eastwest') {
            graph.container.appendChild(arrowUp);
            graph.container.appendChild(arrowDown);
          }

          graph.container.appendChild(arrowRight);
          graph.container.appendChild(arrowLeft);
        }

        // Hides handle for cell under mouse
        if (state != null) {
          currentStateHandle = graph.selectionCellsHandler.getHandler(state.cell);

          if (currentStateHandle != null && currentStateHandle.setHandlesVisible != null) {
            currentStateHandle.setHandlesVisible(false);
          }
        }

        activeTarget = true;
      }
      else {
        let elts = [roundSource, roundTarget, arrowUp, arrowRight, arrowDown, arrowLeft];

        for (let i = 0; i < elts.length; i++) {
          if (elts[i].parentNode != null) {
            elts[i].parentNode.removeChild(elts[i]);
          }
        }
      }
    }

    if (!activeTarget && currentStateHandle != null) {
      currentStateHandle.setHandlesVisible(true);
    }

    // Handles drop target
    let target = ((!mxEvent.isAltDown(evt) || mxEvent.isShiftDown(evt)) &&
      !(currentStyleTarget != null && activeArrow == styleTarget)) ?
      mxDragSource.prototype.getDropTarget.apply(this, arguments) : null;
    let model = graph.getModel();

    if (target != null) {
      if (activeArrow != null || !graph.isSplitTarget(target, cells, evt)) {
        // Selects parent group as drop target
        while (target != null && !graph.isValidDropTarget(target, cells, evt) &&
          model.isVertex(model.getParent(target))) {
          target = model.getParent(target);
        }

        if (target != null && (graph.view.currentRoot == target ||
          (!graph.isValidRoot(target) &&
            graph.getModel().getChildCount(target) == 0) ||
          graph.isCellLocked(target) || model.isEdge(target) ||
          !graph.isValidDropTarget(target, cells, evt))) {
          target = null;
        }
      }
    }

    return target;
  });

  dragSource.stopDrag = function () {
    mxDragSource.prototype.stopDrag.apply(this, arguments);

    let elts = [roundSource, roundTarget, styleTarget, arrowUp, arrowRight, arrowDown, arrowLeft];

    for (let i = 0; i < elts.length; i++) {
      if (elts[i].parentNode != null) {
        elts[i].parentNode.removeChild(elts[i]);
      }
    }

    if (currentTargetState != null && currentStateHandle != null) {
      currentStateHandle.reset();
    }

    currentStateHandle = null;
    currentTargetState = null;
    currentStyleTarget = null;
    styleTargetParent = null;
    activeArrow = null;
  };

  return dragSource
}

Graph.prototype.getDropAndConnectGeometry = function (source, target, direction, targets) {
  let graph = this.graph;
  let view = graph.view;
  let keepSize = targets.length > 1;
  let geo = graph.getCellGeometry(source);
  let geo2 = graph.getCellGeometry(target);

  if (geo != null && geo2 != null) {
    geo2 = geo2.clone();

    if (graph.model.isEdge(source)) {
      let state = graph.view.getState(source);
      let pts = state.absolutePoints;
      let p0 = pts[0];
      let pe = pts[pts.length - 1];

      if (direction == mxConstants.DIRECTION_NORTH) {
        geo2.x = p0.x / view.scale - view.translate.x - geo2.width / 2;
        geo2.y = p0.y / view.scale - view.translate.y - geo2.height / 2;
      }
      else {
        geo2.x = pe.x / view.scale - view.translate.x - geo2.width / 2;
        geo2.y = pe.y / view.scale - view.translate.y - geo2.height / 2;
      }
    }
    else {
      if (geo.relative) {
        let state = graph.view.getState(source);
        geo = geo.clone();
        geo.x = (state.x - view.translate.x) / view.scale;
        geo.y = (state.y - view.translate.y) / view.scale;
      }

      let length = graph.defaultEdgeLength;

      // Maintains edge length
      if (graph.model.isEdge(target) && geo2.getTerminalPoint(true) != null &&
        geo2.getTerminalPoint(false) != null) {
        let p0 = geo2.getTerminalPoint(true);
        let pe = geo2.getTerminalPoint(false);
        let dx = pe.x - p0.x;
        let dy = pe.y - p0.y;

        length = Math.sqrt(dx * dx + dy * dy);

        geo2.x = geo.getCenterX();
        geo2.y = geo.getCenterY();
        geo2.width = 1;
        geo2.height = 1;

        if (direction == mxConstants.DIRECTION_NORTH) {
          geo2.height = length
          geo2.y = geo.y - length;
          geo2.setTerminalPoint(new mxPoint(geo2.x, geo2.y), false);
        }
        else if (direction == mxConstants.DIRECTION_EAST) {
          geo2.width = length
          geo2.x = geo.x + geo.width;
          geo2.setTerminalPoint(new mxPoint(geo2.x + geo2.width, geo2.y), false);
        }
        else if (direction == mxConstants.DIRECTION_SOUTH) {
          geo2.height = length
          geo2.y = geo.y + geo.height;
          geo2.setTerminalPoint(new mxPoint(geo2.x, geo2.y + geo2.height), false);
        }
        else if (direction == mxConstants.DIRECTION_WEST) {
          geo2.width = length
          geo2.x = geo.x - length;
          geo2.setTerminalPoint(new mxPoint(geo2.x, geo2.y), false);
        }
      }
      else {
        // Try match size or ignore if width or height < 45 which
        // is considered special enough to be ignored here
        if (!keepSize && geo2.width > 45 && geo2.height > 45 &&
          geo.width > 45 && geo.height > 45) {
          geo2.width = geo2.width * (geo.height / geo2.height);
          geo2.height = geo.height;
        }

        geo2.x = geo.x + geo.width / 2 - geo2.width / 2;
        geo2.y = geo.y + geo.height / 2 - geo2.height / 2;

        if (direction == mxConstants.DIRECTION_NORTH) {
          geo2.y = geo2.y - geo.height / 2 - geo2.height / 2 - length;
        }
        else if (direction == mxConstants.DIRECTION_EAST) {
          geo2.x = geo2.x + geo.width / 2 + geo2.width / 2 + length;
        }
        else if (direction == mxConstants.DIRECTION_SOUTH) {
          geo2.y = geo2.y + geo.height / 2 + geo2.height / 2 + length;
        }
        else if (direction == mxConstants.DIRECTION_WEST) {
          geo2.x = geo2.x - geo.width / 2 - geo2.width / 2 - length;
        }

        // Adds offset to match cells without connecting edge
        if (graph.model.isEdge(target) && geo2.getTerminalPoint(true) != null &&
          target.getTerminal(false) != null) {
          let targetGeo = graph.getCellGeometry(target.getTerminal(false));

          if (targetGeo != null) {
            if (direction == mxConstants.DIRECTION_NORTH) {
              geo2.x -= targetGeo.getCenterX();
              geo2.y -= targetGeo.getCenterY() + targetGeo.height / 2;
            }
            else if (direction == mxConstants.DIRECTION_EAST) {
              geo2.x -= targetGeo.getCenterX() - targetGeo.width / 2;
              geo2.y -= targetGeo.getCenterY();
            }
            else if (direction == mxConstants.DIRECTION_SOUTH) {
              geo2.x -= targetGeo.getCenterX();
              geo2.y -= targetGeo.getCenterY() - targetGeo.height / 2;
            }
            else if (direction == mxConstants.DIRECTION_WEST) {
              geo2.x -= targetGeo.getCenterX() + targetGeo.width / 2;
              geo2.y -= targetGeo.getCenterY();
            }
          }
        }
      }
    }
  }

  return geo2
}

Graph.prototype.isDropStyleEnabled = function (cells, firstVertex) {
  let result = true;

  if (firstVertex != null && cells.length == 1) {
    let vstyle = this.graph.getCellStyle(cells[firstVertex]);

    if (vstyle != null) {
      result = mxUtils.getValue(vstyle, mxConstants.STYLE_STROKECOLOR, mxConstants.NONE) != mxConstants.NONE ||
        mxUtils.getValue(vstyle, mxConstants.STYLE_FILLCOLOR, mxConstants.NONE) != mxConstants.NONE;
    }
  }

  return result;
}

Graph.prototype.isDropStyleTargetIgnored = function (state) {
  return this.graph.isSwimlane(state.cell) || this.graph.isTableCell(state.cell) ||
    this.graph.isTableRow(state.cell) || this.graph.isTable(state.cell);
}

Graph.prototype.updateShapes = function (source, targets) {
  let graph = this.graph;
  let sourceCellStyle = graph.getCellStyle(source);
  let result = [];

  graph.model.beginUpdate();
  try {
    let cellStyle = graph.getModel().getStyle(source);

    // Lists the styles to carry over from the existing shape
    let styles = ['shadow', 'dashed', 'dashPattern', 'fontFamily', 'fontSize', 'fontColor', 'align', 'startFill',
      'startSize', 'endFill', 'endSize', 'strokeColor', 'strokeWidth', 'fillColor', 'gradientColor',
      'html', 'part', 'noEdgeStyle', 'edgeStyle', 'elbow', 'childLayout', 'recursiveResize',
      'container', 'collapsible', 'connectable', 'comic', 'sketch', 'fillWeight', 'hachureGap',
      'hachureAngle', 'jiggle', 'disableMultiStroke', 'disableMultiStrokeFill',
      'fillStyle', 'curveFitting', 'simplification', 'sketchStyle'];

    for (let i = 0; i < targets.length; i++) {
      let targetCell = targets[i];

      if ((graph.getModel().isVertex(targetCell) == graph.getModel().isVertex(source)) ||
        (graph.getModel().isEdge(targetCell) == graph.getModel().isEdge(source))) {
        let style = graph.getCurrentCellStyle(targets[i]);
        graph.getModel().setStyle(targetCell, cellStyle);

        // Removes all children of composite cells
        if (mxUtils.getValue(style, 'composite', '0') == '1') {
          let childCount = graph.model.getChildCount(targetCell);

          for (let j = childCount; j >= 0; j--) {
            graph.model.remove(graph.model.getChildAt(targetCell, j));
          }
        }

        // Replaces the participant style in the lifeline shape with the target shape
        if (style[mxConstants.STYLE_SHAPE] == 'umlLifeline' &&
          sourceCellStyle[mxConstants.STYLE_SHAPE] != 'umlLifeline') {
          graph.setCellStyles(mxConstants.STYLE_SHAPE, 'umlLifeline', [targetCell]);
          graph.setCellStyles('participant', sourceCellStyle[mxConstants.STYLE_SHAPE], [targetCell]);
        }

        for (let j = 0; j < styles.length; j++) {
          let value = style[styles[j]];

          if (value != null) {
            graph.setCellStyles(styles[j], value, [targetCell]);
          }
        }

        result.push(targetCell);
      }
    }
  }
  finally {
    graph.model.endUpdate();
  }

  return result;
}

/**
 * Creates a drag source for the given element.
 */
Graph.prototype.dropAndConnect = function (source, targets, direction, dropCellIndex, evt) {
  let geo = this.getDropAndConnectGeometry(source, targets[dropCellIndex], direction, targets);

  // Targets without the new edge for selection
  let tmp = [];

  if (geo != null) {
    let graph = this.editorUi.editor.graph;
    let editingCell = null;

    graph.model.beginUpdate();
    try {
      let sourceGeo = graph.getCellGeometry(source);
      let geo2 = graph.getCellGeometry(targets[dropCellIndex]);

      // Handles special case where target should be ignored for stack layouts
      let targetParent = graph.model.getParent(source);
      let validLayout = true;

      // Ignores parent if it has a stack layout or if it is a table or row
      if (graph.layoutManager != null) {
        let layout = graph.layoutManager.getLayout(targetParent);

        // LATER: Use parent of parent if valid layout
        if (layout != null && layout.constructor == mxStackLayout) {
          validLayout = false;
        }
      }

      // Checks if another container is at the drop location
      let tmp = (graph.model.isEdge(source)) ? null : graph.view.getState(targetParent);
      let dx = 0;
      let dy = 0;

      // Offsets by parent position
      if (tmp != null) {
        let offset = tmp.origin;
        dx = offset.x;
        dy = offset.y;

        let pt = geo.getTerminalPoint(false);

        if (pt != null) {
          pt.x += offset.x;
          pt.y += offset.y;
        }
      }

      let useParent = !graph.isTableRow(source) && !graph.isTableCell(source) &&
        (graph.model.isEdge(source) || (sourceGeo != null &&
          !sourceGeo.relative && validLayout));

      let tempTarget = graph.getCellAt((geo.x + dx + graph.view.translate.x) * graph.view.scale,
        (geo.y + dy + graph.view.translate.y) * graph.view.scale, null, null, null, function (state) {
          return !graph.isContainer(state.cell);
        });

      if (tempTarget != null && tempTarget != targetParent) {
        tmp = graph.view.getState(tempTarget);

        // Offsets by new parent position
        if (tmp != null) {
          let offset = tmp.origin;
          targetParent = tempTarget;
          useParent = true;

          if (!graph.model.isEdge(source)) {
            geo.x -= offset.x - dx;
            geo.y -= offset.y - dy;
          }
        }
      }
      else if (!validLayout || graph.isTableRow(source) || graph.isTableCell(source)) {
        geo.x += dx;
        geo.y += dy;
      }

      dx = geo2.x;
      dy = geo2.y;

      // Ignores geometry of edges
      if (graph.model.isEdge(targets[dropCellIndex])) {
        dx = 0;
        dy = 0;
      }

      targets = graph.importCells(targets, (geo.x - (useParent ? dx : 0)),
        (geo.y - (useParent ? dy : 0)), (useParent) ? targetParent : null);
      tmp = targets;

      if (graph.model.isEdge(source)) {
        // Adds new terminal to edge
        // LATER: Push new terminal out radially from edge start point
        graph.model.setTerminal(source, targets[dropCellIndex],
          direction == mxConstants.DIRECTION_NORTH);
      }
      else if (graph.model.isEdge(targets[dropCellIndex])) {
        // Adds new outgoing connection to vertex and clears points
        graph.model.setTerminal(targets[dropCellIndex], source, true);
        let geo3 = graph.getCellGeometry(targets[dropCellIndex]);
        geo3.points = null;

        if (geo3.getTerminalPoint(false) != null) {
          geo3.setTerminalPoint(geo.getTerminalPoint(false), false);
        }
        else if (useParent && graph.model.isVertex(targetParent)) {
          // Adds parent offset to other nodes
          let tmpState = graph.view.getState(targetParent);
          let offset = (tmpState.cell != graph.view.currentRoot) ?
            tmpState.origin : new mxPoint(0, 0);

          graph.cellsMoved(targets, offset.x, offset.y, null, null, true);
        }
      }
      else {
        geo2 = graph.getCellGeometry(targets[dropCellIndex]);
        dx = geo.x - Math.round(geo2.x);
        dy = geo.y - Math.round(geo2.y);
        geo.x = Math.round(geo2.x);
        geo.y = Math.round(geo2.y);
        graph.model.setGeometry(targets[dropCellIndex], geo);
        graph.cellsMoved(targets, dx, dy, null, null, true);
        tmp = targets.slice();
        editingCell = (tmp.length == 1) ? tmp[0] : null;
        targets.push(graph.insertEdge(null, null, '', source, targets[dropCellIndex],
          graph.createCurrentEdgeStyle()));
      }

      if (evt == null || !mxEvent.isShiftDown(evt)) {
        graph.fireEvent(new mxEventObject('cellsInserted', 'cells', targets));
      }
    }
    catch (e) {
      this.editorUi.handleError(e);
    }
    finally {
      graph.model.endUpdate();
    }

    if (graph.editAfterInsert && evt != null && mxEvent.isMouseEvent(evt) &&
      editingCell != null) {
      window.setTimeout(function () {
        graph.startEditing(editingCell);
      }, 0);
    }
  }

  return tmp;
}

Graph.prototype.addClickHandler = function (elt, ds, cells) {
  let graph = this.graph;
  let oldMouseDown = ds.mouseDown;
  let oldMouseMove = ds.mouseMove;
  let oldMouseUp = ds.mouseUp;
  let tol = graph.tolerance;
  let first = null;
  let sb = this;

  ds.mouseDown = function (evt) {
    oldMouseDown.apply(this, arguments);
    first = new mxPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));

    if (this.dragElement != null) {
      this.dragElement.style.display = 'none';
      mxUtils.setOpacity(elt, 50);
    }
  };

  ds.mouseMove = function (evt) {
    if (this.dragElement != null && this.dragElement.style.display == 'none' &&
      first != null && (Math.abs(first.x - mxEvent.getClientX(evt)) > tol ||
        Math.abs(first.y - mxEvent.getClientY(evt)) > tol)) {
      this.dragElement.style.display = '';
      mxUtils.setOpacity(elt, 100);
    }
    ds.getDropTarget(graph)
    oldMouseMove.apply(this, arguments);
  };

  ds.mouseUp = function (evt) {
    try {
      if (!mxEvent.isPopupTrigger(evt) && this.currentGraph == null &&
        this.dragElement != null && this.dragElement.style.display == 'none') {
        sb.itemClicked(cells, ds, evt, elt);
      }

      oldMouseUp.apply(ds, arguments);
      mxUtils.setOpacity(elt, 100);
      first = null;

      // Blocks tooltips on this element after single click
      sb.currentElt = elt;
    }
    catch (e) {
      ds.reset();
      console.log(e)
    }
  }
}

Graph.prototype.isContainer = function (cell) {
  let style = this.graph.getCurrentCellStyle(cell);

  if (this.graph.isSwimlane(cell)) {
    return style['container'] != '0';
  }
  else {
    return style['container'] == '1';
  }
}

export default Graph